iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
Software Development

螃蟹幼幼班:Rust 入門指南系列 第 17

Day17 - 方法與關聯函數

  • 分享至 

  • xImage
  •  

簡介

我們可以用impl(implementation)關鍵字,後面接定義好的型別名稱,接著用大括號隔出一個區塊,這個區塊內的內容都會和這個型別有關,在這個區塊中Self 型別是該impl區塊要實作型別的別名。

裡面可以用 fn 建立函數,不過這邊和一般的函數不同,大致可以分為實例才能使用的方法(method),和型別才能使用的關聯函數(associated function),兩者最大的差別在於方法需要通過實例來調用,所以第一個參數永遠是 self,代表的是呼叫該方法的實例本身。

方法

首先說明方法,方法參數的 self 其實大部分的情況我們會用 &self ,這是self: &Self 的簡寫,也就是這個型別的參考,原因是大部分的情況我們不想取得實例的所有權,官方提到只使用 self 而取得所有權的方法更是非常少見,這種使用技巧通常是為了想改變 self本身,並且希望避免被改變的實例繼續被呼叫。而如果要透過方法改變實例上的數值的話我們會用 &mut self(對應型別的可變參考)。

使用方法而不使用函數最大的原因是,這樣在組織或表達上都更清楚這些方法都是專門給這個型別用的,而且這樣寫可以把邏輯集中在一起,同時不必在方法簽名重複 self 的型別,用函數的話就要在每個函數簽名都要寫上這個型別的名稱。

基本方法

我們先用昨天介紹的結構體來當範例。
我們試著定義一個 struct Car,它的屬性有顏色、座標還有速度,座標我們用上一篇講到的tuple structs 來定義,結構體的屬性型別也可以是另外一個結構體,讓它的使用變得很彈性。

另外我們也實作一個回傳車子顏色的方法(color)。

struct Position(i32, i32); // (x, y) 座標

struct Car {
    color: String,
    speed: u32,
    position: Position,
}

impl Car {
    fn color(&self) -> &str {
        &self.color
    }
}

fn main() {
    let car = Car {
        color: String::from("Red"),
        speed: 0,
        position: Position(3, 4),
    };

    println!("The car's color is: {}", car.color());
}

自動解參考(Automatic Dereferencing)

我們用上面那段程式來解釋 Rust 中的自動解參考特性。仔細看 car.color() 的部分,對應的方法簽名中的第一個參數是 &self。雖然原本這樣寫會讓你認為兩者型別不符合,實際上 Rust 在呼叫方法時會自動將 car 轉換為引用(例如 &car),因此我們不需要手動寫成 (&car).color()。這種自動處理的行為使得我們可以直接用實體來呼叫方法,這就是 Rust 的自動參考機制。

除了呼叫方法,還有索引操作、解構也有自動解參考的特性,使得這些操作更加簡潔和直觀。

改變實例屬性的方法

我們再來實作會改變實例屬性的方法,讓車子可以改變速度並根據速度移動,然後再加上取得車子目前座標的方法(position)。

impl Car {
    fn color(&self) -> &str {
        &self.color
    }

    fn position(&self) -> &Position {
        &self.position
    }

    fn speed_up(&mut self) {
        self.speed += 1;
    }

    fn move_up(&mut self) {
        self.position.1 += self.speed as i32;
    }

    fn move_down(&mut self) {
        self.position.1 -= self.speed as i32;
    }

    fn move_right(&mut self) {
        self.position.0 += self.speed as i32;
    }

    fn move_left(&mut self) {
        self.position.0 -= self.speed as i32;
    }
}
fn main() {
    let mut car = Car {
        color: String::from("Red"),
        speed: 0,
        position: Position(3, 4),
    };

    car.speed_up();
    car.move_up();

    println!("The car's color is: {}", car.color());
    println!("The car's position is: {:?}", car.position());
}

需要注意的幾個地方,會改變本身屬性的方法的參數要用可變參考 &mut self,另外在宣告變數的時候要記得加上 mut 關鍵字,這個實例才能透過這些方法去改變屬性,不然會報錯。

印出自訂義型別資訊

即便已經修改了 &mut 的部分,我們還會遇到一個與 Position 型別相關的錯誤。

error[E0277]: `Position` doesn't implement `Debug`
  --> src/main.rs:50:45
   |
50 |     println!("The car's position is: {:?}", car.position());
   |                                             ^^^^^^^^^^^^^^ `Position` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Position`, which is required by `&Position: Debug`
   = note: add `#[derive(Debug)]` to `Position` or manually `impl Debug for Position`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Position` with `#[derive(Debug)]`
   |
1  + #[derive(Debug)]
2  | struct Position(i32, i32); // (x, y) 座標
   |

這個錯誤是因為我們要用 println! 把資料印到終端上的時候,我們使用 {:?} 格式化指定器時,Rust 會嘗試將對應的資料轉換成可讀的格式。
這個轉換過程需要資料的型別實作 Debug 特徵,但因為 Position 是我們自訂的型別,所以沒有實作這個特徵。特徵的介紹我們會放到之後再介紹,目前只需要知道它可以為不同的型別定義一組共同的方法。

如果我們不論如何都需要知道Position的資訊,那現在我們會需要實作 Debug 特徵。有兩個方式可以做到這件事情:

  1. 使用 #[derive(Debug)] 屬性:

直接在 Position 結構體的定義上方加入 #[derive(Debug)] 屬性,讓編譯器自動實作 Debug 特徵,也是編譯器裡建議的方式,這已經可以涵蓋大部分的需求。

#[derive(Debug)]
struct Position(i32, i32); // (x, y) 座標
  1. 手動實作Debug特徵(需要自定義輸出的情況),不過目前還沒介紹到特徵就先省略。

處理完之後我們就可以把位置資訊也印出來啦。

$ cargo run
The car's color is: Red
The car's position is: Position(3, 5)

關聯函數

最後介紹關聯函數,最常見的用法是用來初始化和創建該型別的實例,因為 Rust 中,沒有特定語法來定義「建構函數」,像是 Python 的 class 的 __init__ 又或是 JavaScript 的 constructor,所以前面我們在建立結構體的實體才會直接用大括號來初始化,不過 Rust 習慣上會使用一個關聯函數來實現類似其他語言的建構函數,習慣上會用 new 作為函數名稱,但沒有強制規定。

我們在 impl Car 裡多加上這一段,如前面所說Self 代表的是當前 impl 區塊對應的型別,雖然 SelfCar 在這邊可以互換,但未來如果型別名稱改變,用 Self 的寫法就代表對應型別,就不需要把整個程式碼有用到的地方全部替換,也可以避免漏改的風險。

    fn new(color: String) -> Self {
        Self {
            color,
            speed: 0,
            position: Position(0, 0),
        }
    }

多一層類似建構函數的好處是我們可以根據不同情況定義不同的關聯函數,建立實例的時候可以簡化一些邏輯或不需要那麼多設定,以上面的例子來說,它限制了初始化的位置和速度,外部在建立實例的時候簡化到提供顏色就可以,初始位置也統一了。

因為關聯函數不像方法是透過實體呼叫,呼叫關聯函數的方式是在該型別後面加上 :: 再接函數名稱。像這個函數用是用結構體名稱(Car)作為命名空間:

    let mut car = Car::new(String::from("Red"));

看到上面的程式碼應該有發現,之前一直在用的 String::from也是關聯函數,用來建立新的 String 實體,另外String::new則是另外一個可以建立 String 實體的關聯函數。

關聯函數不只是建立實例,也可以實作其他彈性的功能,例如我要知道兩台車之間的距離有多遠,可以在 impl Car 多加上一個關聯函數 distance_between:

    fn distance_between(car1: &Car, car2: &Car) -> f64 {
        let x_diff = (car1.position.0 - car2.position.0).pow(2);
        let y_diff = (car1.position.1 - car2.position.1).pow(2);
        ((x_diff + y_diff) as f64).sqrt()
    }

接著在 main 裡呼叫它:

fn main() {
    let mut car1 = Car::new(String::from("Red"));
    let mut car2 = Car::new(String::from("Blue"));

    car1.speed_up();
    car2.speed_up();

    car1.move_up();
    car1.move_up();

    car2.move_right();
    car2.move_right();
    car2.move_down();

    println!("Car 1 - Color: {}, Position: {:?}", car1.color(), car1.position());
    println!("Car 2 - Color: {}, Position: {:?}", car2.color(), car2.position());

    let distance = Car::distance_between(&car1, &car2);
    println!("Distance between car 1 and car 2: {}", distance);
}

執行結果:

$ cargo run
Car 1 - Color: Red, Position: Position(0, 2)
Car 2 - Color: Blue, Position: Position(2, -1)
Distance between car 1 and car 2: 3.605551275463989

結語

在本篇文章我們介紹了 Rust 中的方法和關聯函數這兩個重要概念:

  • impl區塊中,我們可以定義與特定型別相關的函數。
  • 這些函數又分為兩類:
    1. 方法:需要通過實例調用,第一個參數通常是 &self&mut self
    2. 關聯函數:直接通過型別名稱調用,如 String::from()

重要的是,這些概念不僅適用於結構體,而是可以應用在 Rust 中的所有型別。
無論是內建型別還是自定義型別,都可以擁有自己的方法和關聯函數。

結構體是 Rust 自定義型別的兩大支柱之一,通過結構體、方法和關聯函數,我們能夠:

  • 將相關聯的資料集中在一起。
  • 確保資料各部分之間的關聯性。
  • 將處理邏輯集中,提高程式碼的可讀性和組織性。
    這種組織方式不僅讓我們的程式碼更加結構化,也為後續的維護和擴展奠定了基礎。

在下一篇中,我們將介紹 Rust 自定義型別的另一個重要支柱:列舉(enumerations)。列舉將為我們提供另一種強大的工具,用於表示可能有多個不同狀態的資料。


上一篇
Day16 - 結構體
下一篇
Day18 - 列舉與 match
系列文
螃蟹幼幼班:Rust 入門指南25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言